﻿/** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
**/

package player {
	import flash.net.NetStream;
	import flash.net.NetConnection;
	import flash.events.EventDispatcher;
	import flash.events.NetStatusEvent;
	import flash.utils.Timer;
	import flash.events.TimerEvent;
	import flash.events.AsyncErrorEvent;
	import flash.events.Event;
	import flash.events.SecurityErrorEvent;
	import flash.events.IOErrorEvent;
	import flash.utils.setTimeout;
	import flash.external.ExternalInterface;
	
	/**
	 * Fired after video bitrate is changed
	 */
	[Event(name="videoBitrateChanged", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after video is initialized and ready to be attached
	 */
	[Event(name="videoInitialized", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired once a seconds if video playhead time changes
	 */
	[Event(name="videoPlayheadTimeChanged", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after seeking is complete
	 */
	[Event(name="seekingComplete", type="player.WebTVVideoStatusEvent")]
	
	
	/**
	 * Fired after video playback is started
	 */
	[Event(name="playbackStarted", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after video playback is paused
	 */
	[Event(name="playbackPaused", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after video playback is resumed after pause
	 */
	[Event(name="playbackResumed", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after video playback is stopped (NOT completed!)
	 */
	[Event(name="playbackStopped", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after video playback is completed
	 */
	[Event(name="playbackCompleted", type="player.WebTVVideoStatusEvent")]
	
	/**
	 * Fired after video replay is started
	 */
	[Event(name="playbackReplaying", type="player.WebTVVideoStatusEvent")]
	
	/*
	 * Fired on playback error
	 */
	[Event(name="playbackError", type="player.WebTVVideoStatusEvent")]
	
	public class WebTVPlayer extends EventDispatcher {
		
		/*-........................Constants..................................*/
		
		/*-........................Properties.................................*/
		private var _videos:Array; // of WebTVVideo
		private var _firstVideo:WebTVVideo;
		
		private var _advertisement:WebTVVideo;
		
		private var _stream:NetStream;
		
		/**
		 * Used only if progressive download
		 */
		private var _netStream:NetStream;
		
		/**
		 * Used only if streaming
		 */
		private var _dynamicStream:DynamicStream;
		
		private var _connection:NetConnection;
		private var _duration:Number;
		private var _keyFrameTimes:Object;
		private var _keyFrameFilePositions:Object;
		private var _serverSeekBytesStart:Number = 0;
		private var _wholeVideoBytesTotal:Number;
		private var _bufferingComplete:Boolean;
		private var _playbackStartTime = 0;
		private var _isStopStartSeek:Boolean = false;
		
		private var _currentBitrate:Number;
		private var _bitratesAvailable:Array; // of Number
		
		/**
		 * Helper variable defined whether the previous seek was done inside
		 * a buffered area
		 */
		private var _didBufferedSeek:Boolean;
		
		private var _videoStatusTimer:Timer;
		
		private var _prevPlayheadTime:Number;
		private var _prevBytesLoaded:Number;
		
		private var _previousSeekTo:Number;
		
		private var _isPlaying:Boolean;
		private var _isCompleted:Boolean;
		private var _resumePlaybackAfterSeek:Boolean;
		
		private var _profiler:PlayerProfiler = PlayerProfiler.getInstance();
		
		/**
		 * If this variable is bigger than zero, the actual 
		 * stream time is not used for playhead change events. Instead of the stream time the 
		 * _previousSeek time is used because right after the seek the stream time is wrong.
		 *
		 * This number indicates how many times the helper is used instead of real 
		 * stream time
		 */
		private var _countHelperTimeUsedAfterSeek:uint = 0;
		private static const SECONDS_HELPER_USED_AFTER_SEEK:uint = 2;
		
		private var _cachedPlayheadChangedEvent:WebTVVideoStatusEvent;
		private var _cachedBufferChangedEvent:WebTVVideoStatusEvent;
		
		/**
		 * Logging on/off. Turn this of for production.
		 */
		private var _logging:Boolean = true;
		
		/**
		 * Helper variable for detecting when the playback is really compleated.
		 */
		private var _isStreamStopping:Boolean;

		/*-........................Constructor................................*/
		
		/**
		 * Player constructor
		 *
		 * @param videos Array of video links. Array is used for playing multiple
		 * bitrates of a video
		 */
		public function WebTVPlayer(videos:Array) {
			// var uniqueIdentifier:String = _profiler.start();
			this.videos = videos;
			// _profiler.end(uniqueIdentifier);
		}
		
		public function set videos(videos:Array){
			if(videos != null){
				_cleanUp();
			}
			
			_videos = videos;
			_firstVideo = _videos[0];
			_currentBitrate = _firstVideo.bitrate;
		}
		
		public function initialize():void{
			// var uniqueIdentifier:String = _profiler.start();
			_log("Initializing video");
			
			_initializeVideoStatusTimer();
			_initializeVideo();

			// _profiler.end(uniqueIdentifier);
		}
		
		private function _initializeVideoStatusTimer():void{
			// var uniqueIdentifier:String = _profiler.start();
			_prevPlayheadTime = 0;
			_prevBytesLoaded = 0;
			
			if(_videoStatusTimer == null){
				_videoStatusTimer = new Timer(1000);
				_videoStatusTimer.addEventListener(TimerEvent.TIMER, this._videoStatus);
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _initializeVideo():void{
			// var uniqueIdentifier:String = _profiler.start();
			if(_firstVideo.isStreaming){
				_initializeStreamingVideo();
			} else {
				_initializeProgressiveVideo();
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _addStreamEventHandlers():void{
			// Add stream listeners
			// var uniqueIdentifier:String = _profiler.start();
			_stream.addEventListener(NetStatusEvent.NET_STATUS, _videoStopHandler);
			_stream.addEventListener(NetStatusEvent.NET_STATUS, _afterSeekingListenerHandler);
			_stream.addEventListener(NetStatusEvent.NET_STATUS, _videoBufferingChanged);
			_stream.addEventListener(NetStatusEvent.NET_STATUS, _streamNetStatusHandler);
			// _profiler.end(uniqueIdentifier);
		}
		
		/**
		 * Initializes progressive (=not streaming) video.
		 *
		 * Video array is used only for streaming videos, so that's 
		 * why firstVideo is used in case of progressive video
		 */
		private function _initializeProgressiveVideo():void{
			// var uniqueIdentifier:String = _profiler.start();
			_log("Initializing progressive video")
			
			_connection = new NetConnection();
			_connection.connect(null);
			_netStream = new NetStream(_connection);
			
			var client:Object = new Object();
			client.onMetaData = function(meta:Object){
				_duration = meta.duration;
				
				var keyframes:Object = meta.keyframes;
				
				if(keyframes){
					if(keyframes.times){
						_keyFrameTimes = keyframes.times;
					}
		
					if(keyframes.filepositions){
						_keyFrameFilePositions = keyframes.filepositions;
					}
				}
				
				if(isNaN(_firstVideo.bytePosition) || _firstVideo.bytePosition == 0){
					_wholeVideoBytesTotal = _netStream.bytesTotal;
				}
				
				// Dump metadata (for testing only)
				/*
				var dump:Function = function(obj:*, level:int = 0, output:String = ""):* {
					var tabs:String = "";
					for(var i:int = 0; i < level; i++, tabs += "\t")

					for(var child:* in obj) {
						output += tabs +"["+ child +"] => "+ obj[child];
						var childOutput:String = dump(obj[child], level+1);
						if(childOutput != '') output += ' {\n'+ childOutput + tabs +'}';
						output += "\n";
					}
					if(level == 0) trace(output);
					else return output;
				}*/
				
				// TESTING dump(meta)
			}
		
			_netStream.client = client;
			_netStream.bufferTime = 5;
			
			_stream = _netStream;
			
			_addStreamEventHandlers();
			_dispatchVideoInitializedEvent();
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _initializeStreamingVideo():void{
			// var uniqueIdentifier:String = _profiler.start();
			_log("Initialize streaming video");
			_connection = new NetConnection();
			
			var client:Object = new Object();
			client.onBWDone = function():void{
				_log("onBWDone");
			}
			_connection.client = client;
			
			_connection.addEventListener(NetStatusEvent.NET_STATUS, function(event:NetStatusEvent):void{
           		_log("NetConnection NetStatusHandler: " + event.info.code);
				
				switch (event.info.code) {
                	case "NetConnection.Connect.Success":
						_dynamicStream = new DynamicStream(_connection);
						_dynamicStream.bufferTime = 2; 
			
						var client:Object = new Object(); 
						client.onMetaData = function(meta:Object){
							_duration = meta.duration;
						}
						_dynamicStream.client = client;
						_dynamicStream.manualSwitchMode(firstVideo.isDynamic);
			
						_stream = _dynamicStream;
						
						_addStreamEventHandlers();
						_dispatchVideoInitializedEvent();
                    	break;
                	case "NetStream.Play.StreamNotFound":
                    	_log("Stream not found:");
                    	break;
            	}
			});
			
			_connection.addEventListener(AsyncErrorEvent.ASYNC_ERROR, function(event:AsyncErrorEvent):void{
				_log("AsyncErrorEvent"); 
			});
			
			_connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event:SecurityErrorEvent):void {
            	_log("securityErrorHandler: " + event);
        	});
			
			_connection.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void {
            	_log("ioErrorEvent: " + event);
        	});
			
			_log("Connecting to streaming server " + _firstVideo.streamServerUrl);
			
			_connection.connect(_firstVideo.streamServerUrl);
			// _profiler.end(uniqueIdentifier);
		}
		
		
		private function _dispatchVideoInitializedEvent():void{
			// var uniqueIdentifier:String = _profiler.start();
			_log("Dispatching video initialized event");
			var event:WebTVVideoStatusEvent = new WebTVVideoStatusEvent(WebTVVideoStatusEvent.VIDEO_INITIALIZED);
			dispatchEvent(event);
			// _profiler.end(uniqueIdentifier);
		}
		
		/*-........................Methods....................................*/
		
		private function _cleanUp():void{
		
			_stream = null;
			_netStream = null;
			_dynamicStream = null;
			
			_connection = null;
			_duration = NaN;
			_keyFrameTimes = null;
			_keyFrameFilePositions = null;
			_serverSeekBytesStart = 0;
			_wholeVideoBytesTotal = NaN
			_bufferingComplete = false;
			_currentBitrate = NaN;
			_bitratesAvailable = null;

			_didBufferedSeek = false;
			_prevPlayheadTime = NaN;
			_prevBytesLoaded = NaN;
			_previousSeekTo = NaN;
		
			_isPlaying = false;
			_isCompleted = false;
			_resumePlaybackAfterSeek = false;
		}
		
		/*
		
		Uncomment this if you want to log all the events dispatched by WebTVPlayer
		
		override public function dispatchEvent(e:Event):Boolean{
			// var uniqueIdentifier:String = _profiler.start();
			_log("Dispatching event " + e.toString());
			
			// _profiler.end(uniqueIdentifier);
			return super.dispatchEvent(e);
		}
		*/
		
		private function _videoBufferingChanged(e:NetStatusEvent):void {
			var code:String = e.info.code;
			
			if(code == "NetStream.Buffer.Full" || code == "NetStream.Buffer.Empty"){
				trace("BufferingChanged [code: " + code + ", _isCompleted: " + _isCompleted + ", _isPlaying: " + _isPlaying + "]");
			}
			
			if(code == "NetStream.Buffer.Full"){
				dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.BUFFERING_ENDED));
			}
			if(code == "NetStream.Buffer.Empty" && !_isCompleted){
				// NOTICE: Don't show the buffering indicator in case buffer is empty because 
				// the playback is completed
				
				dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.BUFFERING_STARTED));
			}
		}
		
		private function _videoStatus(e:TimerEvent = null):void {
			// var uniqueIdentifier:String = _profiler.start();
			
			// Current video time
			var time:Number;
			if(_countHelperTimeUsedAfterSeek > 0){
				// Right after seeking the stream time is wrong
				// That's why few seconds after seeking a helper time 
				// is used instead
				time = _previousSeekTo + (SECONDS_HELPER_USED_AFTER_SEEK - _countHelperTimeUsedAfterSeek);
				
				if(_isPlaying){
					// Do not decrease helper time count if stream is not playing
					// Otherwise wrong time is returned
					_countHelperTimeUsedAfterSeek--;
				}
				
			} else {
				time = _playbackStartTime + _stream.time;
			}
			
			if(_prevPlayheadTime != time){
				_isStopStartSeek = false;
				
				// Create event
				_cachedPlayheadChangedEvent = new WebTVVideoStatusEvent(WebTVVideoStatusEvent.VIDEO_PLAYHEAD_TIME_CHANGED);
				_cachedPlayheadChangedEvent.playheadTime = time;
					
				// Update helper variables
				_prevPlayheadTime = time;
					
				// Dispatch
				dispatchEvent(_cachedPlayheadChangedEvent);
			}
			
			// Buffer changed
			var bytesLoaded:uint = _stream.bytesLoaded;
			var bytesTotal:uint = _stream.bytesTotal;
			if(_prevBytesLoaded != bytesLoaded){
				// Create event
				_cachedBufferChangedEvent = new WebTVVideoStatusEvent(WebTVVideoStatusEvent.BUFFER_CHANGED);
				_cachedBufferChangedEvent.bytesLoaded = bytesLoaded;
				_cachedBufferChangedEvent.bytesTotal = bytesTotal;
				_cachedBufferChangedEvent.serverSeekBytesStart = _serverSeekBytesStart;
				_cachedBufferChangedEvent.wholeVideoBytesTotal = _wholeVideoBytesTotal;
				
				// Update
				_prevBytesLoaded = bytesLoaded;
				
				// Dispatch
				dispatchEvent(_cachedBufferChangedEvent);
			}
			
			// _profiler.end(uniqueIdentifier);
		}
		
		/**
 		 * Listens video status after seeking.
		 * This is important because the seeking fails quite often because
		 * the seeking is not done to the key frame.
 		 */
		private function _afterSeekingListenerHandler(e:NetStatusEvent):void{
			// var uniqueIdentifier:String = _profiler.start();
			if(_didBufferedSeek){
				if(e.info.code == "NetStream.Seek.InvalidTime"){
					// Seeking failed because of invalid time
					// Seek to the nearest keyframe
					_stream.seek(e.info.details);
			
					_seekingComplete();
				}

				if(e.info.code == "NetStream.Seek.Notify"){
					// Seeking complete
					_seekingComplete();
				}
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		/**
		 * Listens NetStatusEvent for detecting when the playback is completed.
		 *
		 * NOTICE: If the video is streaming video, the events 
		 * NetStream dispatches are quite confusing. When video is 
		 * near at its end the NetStream.Play.Stop event is dispatched even though
		 * the playback is still on. In this case the NetStream.Play.Stop indicates 
		 * that the rest of the video is loaded in the buffer and buffer is now flushing.
		 * So, in reality, the playback is completed after the buffer is empty.
		 *
		 * NOTICE: For progressive videos NetStream.Play.Stop is dispatched when 
		 * the video playback is really stopped.
		 *
		 * This method uses _isStopping property as a helper variable
		 *
		 * @param e NetStatusEvent status event from NetStream
		 */
		private function _videoStopHandler(e:NetStatusEvent):void{
			// var uniqueIdentifier:String = _profiler.start();
			
			if (_isStopStartSeek) {
				return;
			}
			
			if(_isStreamStopping && e.info.code == "NetStream.Buffer.Empty"){
				_doPlaybackCompleted();
			}
			
			if(e.info.code == "NetStream.Play.Stop"){
				if(firstVideo.isStreaming){
					_isStreamStopping = true;
				} else {
					_doPlaybackCompleted();
				}
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _doPlaybackCompleted():void{
			_isPlaying = false;
			_isCompleted = true;
			_videoStatusTimer.stop();
			_isStreamStopping = false;
			dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_COMPLETED));
		}
		
		private function _seekingComplete():void{
			// var uniqueIdentifier:String = _profiler.start();
			// Resume playback if buffered seek
			_log("Seeking complete: [isPlaying: " + _isPlaying + ", didBufferSeek: " + _didBufferedSeek + ", _resumePlaybackAfterSeek: " + _resumePlaybackAfterSeek + "]")
			if(_didBufferedSeek){
				if(_resumePlaybackAfterSeek){
					setTimeout(function() { _stream.resume(); }, 200);
				}
				_didBufferedSeek = false;
			}
			
			var event:WebTVVideoStatusEvent = new WebTVVideoStatusEvent(WebTVVideoStatusEvent.SEEKING_COMPLETE);
			// _stream.time does not give correct playhead time after seeking. That's why helper variable
			// _previousSeekTo is used
			event.playheadTime = _previousSeekTo;
			dispatchEvent(event);
			
			// Helper time to update playhead time right after seek
			_countHelperTimeUsedAfterSeek = SECONDS_HELPER_USED_AFTER_SEEK;
			// _profiler.end(uniqueIdentifier);
		}
		
		/**
		 * Only for debugging
		 */
		private function _streamNetStatusHandler(e:NetStatusEvent):void{
			// var uniqueIdentifier:String = _profiler.start();
			_log("NetStatusHandler: e.info.code " + e.info.code);
			// _profiler.end(uniqueIdentifier);
			
			ExternalInterface.call("alert", "Error code: " + e.info.code);
			
			if (e.info.code == "NetStream.Play.StreamNotFound") {
				dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_ERROR));
			}
		}
		
		private function _log(msg:String):void{
			// var uniqueIdentifier:String = _profiler.start();
			if(_logging) trace("*** WebTVPlayer: " + msg);
			// _profiler.end(uniqueIdentifier);
		}
		
		/**
		 * Start playing video
		 *
		 * @param url video url. Null by default. This is used for 
		 * server seeking
		 */
		public function play(url:String = null):void{
			_playbackStartTime = 0;
			
			// var uniqueIdentifier:String = _profiler.start();
			if(_firstVideo.isStreaming){
				_playStream();
			} else {
				_playProgressive(url);
			}
			
			// Start video status timer
			_videoStatusTimer.start();
			
			// Dispatch event
			_isPlaying = true;
			_isCompleted = false;
			dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_STARTED));
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _playProgressive(url:String):void{
			// var uniqueIdentifier:String = _profiler.start();
			if(url != null){
				_stream.play(url);
			} else {
				_stream.play(this._firstVideo.link);
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _playStream(pos:Number = -1):void{
			// var uniqueIdentifier:String = _profiler.start();
			var dsi:DynamicStreamItem = new DynamicStreamItem();

			for each (var video:WebTVVideo in _videos){
				dsi.addStream(video.streamName, video.bitrate);
			}
			
			if (pos >= 0) {
				dsi.start = pos;
				_playbackStartTime = pos;
			} else {
				_playbackStartTime = 0;
			}
			
			_dynamicStream.startPlay(dsi);
			// _profiler.end(uniqueIdentifier);
		}
		
		public function stop():void{
			// var uniqueIdentifier:String = _profiler.start();
			_stream.close();
			_videoStatusTimer.stop();
			
			_isPlaying = false;
			_isCompleted = false;
			_log("isPlaying changed to :" + _isPlaying + ", function: stop");
			dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_STOPPED));
			// _profiler.end(uniqueIdentifier);
		}		
		
		public function pause():void{
		
			// var uniqueIdentifier:String = _profiler.start();
			
			if (_stream != null && _isPlaying) {
				_stream.pause();
			
				_isPlaying = false;
				_isCompleted = false;
				_log("isPlaying changed to :" + _isPlaying + ", function: pause");
				
				// When playback is paused, don't show buffering indicator
				dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.BUFFERING_ENDED));
				
				dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_PAUSED));
				
				// _profiler.end(uniqueIdentifier);
				// _profiler.printResults();
			}
		}
		
		public function replay():void{
			// var uniqueIdentifier:String = _profiler.start();
			
			var forceResumeAfterSeek:Boolean = true;
			this.seekTo(0, forceResumeAfterSeek);
			
			_isPlaying = true;
			_isCompleted = false;
			_videoStatusTimer.start();

			dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_REPLAYING));
			// _profiler.end(uniqueIdentifier);
		}
		
		public function resume():void{
			// var uniqueIdentifier:String = _profiler.start();
			_stream.resume();
			
			_isPlaying = true;
			_isCompleted = false;
			
			_log("isPlaying changed to :" + _isPlaying + ", function: resume");
			
			dispatchEvent(new WebTVVideoStatusEvent(WebTVVideoStatusEvent.PLAYBACK_RESUMED));
			// _profiler.end(uniqueIdentifier);
		}
		
		/**
		 * Seeks to given time
		 *
		 * Does seek inside buffered area or server seek if available
		 * Streams are seeked as a "buffered" seek
		 * 
		 * @param time video time in seconds
		 * @param forceResumeAfterSeek depending on whether the video was playing before
		 * seeking the video playback is resumed or not after the seeking. If forceResumeAfterSeek
		 * is true, the playback is in any case resumed after seek. This is useful for example 
		 * if you are doing seeking in a means of replaying the video.
		 */
		public function seekTo(time:Number, forceResumeAfterSeek:Boolean = false){
			_log("Seek to: " + time + " force resume: " + forceResumeAfterSeek);
			
			_isStopStartSeek = false;
			
			// var uniqueIdentifier:String = _profiler.start();
			_previousSeekTo = time;
			
			if (_firstVideo.isStreaming) {
				_doStreamSeek(time);
			} else {
				// Resumes playback after seek if it is forced. Otherwise resumed if
				// video was playing before seek
				_resumePlaybackAfterSeek = forceResumeAfterSeek ? true : _isPlaying;
				
				if(isBuffered(time)){
					_doBufferedSeek(time);
				} else {
					if(_firstVideo.serverSeekAvailable){
						_doServerSeek(time);
					} else {
						_doBufferedSeek(time);
					}
				}
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _bytesToVideoTime(bytes:Number){
			// var uniqueIdentifier:String = _profiler.start();
			
			return (bytes / _wholeVideoBytesTotal) * _duration;
			// _profiler.end(uniqueIdentifier);
		}
		
		public function isBuffered(videoTime){
			// var uniqueIdentifier:String = _profiler.start();
			
			_log("_serverSeekBytesStart: " + _serverSeekBytesStart + ", _stream: " + _stream);
			
			if(_stream == null){
				return false;
			}
			
			var bufferStart:Number = _bytesToVideoTime(_serverSeekBytesStart);
			var bufferEnd:Number = _bytesToVideoTime(_serverSeekBytesStart + _stream.bytesLoaded);
	
			var isBufferedResult:Boolean = videoTime > bufferStart && videoTime < bufferEnd;
	
			return isBufferedResult;
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _doStreamSeek(videoTime) {
			_stream.close();
			
			if (videoTime < duration - 1) {
				_isStopStartSeek = true;
				_playStream(videoTime);
				_resumePlaybackAfterSeek = false;
				_seekingComplete();
			} else {
				_resumePlaybackAfterSeek = false;
				_seekingComplete();
				_doPlaybackCompleted();
			}
		}
		
		/**
		 * Seeking either on the buffered area of a progressive video
		 * or stream seeking
		 */
		private function _doBufferedSeek(videoTime){
			// var uniqueIdentifier:String = _profiler.start();
			_log("doBufferedSeek to: " + videoTime);
	
			_didBufferedSeek = true;
			
			// For the sake of better performance on the mobile device
			// pause the stream before seeking. And for the same reason
			// hide the toolbar.
			_stream.pause();
		
			// If seeking is done to the end of the video, minus this amount of time
			// to prevent from crashing. The time is 3 secs, but if the video is less 
			// than 3 secs, take 20% of the time
			var safeBuffer:Number = _duration < 3 ? _duration * 0.2 : 3;

			if(Math.abs(videoTime - this.bufferEndTime) < 0.5){
				// Seek faíls if seeking is done to the end of the video
				var seekTime:Number = videoTime - safeBuffer;
				_log("Doing buffered seek to " + seekTime);
				_stream.seek(videoTime - safeBuffer);
			} else {
				_log("Doing buffered seek to " + videoTime);
				_stream.seek(videoTime);
			}
			// _profiler.end(uniqueIdentifier);
		}
		
		private function _getSeekBytePosition(seekPosition: Number, rangeBegin:Number = 0, rangeEnd:Number = undefined):Number {
			// var uniqueIdentifier:String = _profiler.start();
			if(seekPosition == 0){
				return 0;
			}
			
			if (!rangeEnd) {
				rangeEnd = _keyFrameTimes.length - 1;
			}
			if (rangeBegin == rangeEnd) { 
				 return _keyFrameFilePositions[rangeBegin];
			}
	
			var rangeMid:Number = Math.floor((rangeEnd + rangeBegin)/2);
			if (_keyFrameTimes[rangeMid] >= seekPosition)
				return _getSeekBytePosition(seekPosition, rangeBegin, rangeMid);
			else
				return _getSeekBytePosition(seekPosition, rangeMid+1, rangeEnd);
				
			// _profiler.end(uniqueIdentifier);
		}

		public function _doServerSeek(videoTime):void{
			// var uniqueIdentifier:String = _profiler.start();
			_log("doServerSeek to: " + videoTime);
			var bytePosition:Number = _getSeekBytePosition(videoTime);
	 		_firstVideo.bytePosition = bytePosition;
			_serverSeekBytesStart = bytePosition;
	
			// Clean up buffering variables
			_bufferingComplete = false;
	
			_stream.play(_firstVideo.link + "?start=" + bytePosition);
			
			if(!_resumePlaybackAfterSeek){
				_stream.pause();
			}
			
			_seekingComplete();
			// _profiler.end(uniqueIdentifier);
		}
		
		public function changeBitrate(bitrate:Number):void{
			// var uniqueIdentifier:String = _profiler.start();
			if(_firstVideo.isStreaming){
				_log("Changing bitrate of the streaming video to " + bitrate);
				_dynamicStream.switchToStreamRate(bitrate);
				
			} else {
				throw new Error("Unimplemented: Changing bitrate of progressive video");
			}
			
			_currentBitrate = bitrate;
			
			var bitrateChangeEvent:WebTVVideoStatusEvent = new WebTVVideoStatusEvent(WebTVVideoStatusEvent.VIDEO_BITRATE_CHANGED);
			bitrateChangeEvent.bitrate = _currentBitrate;
			dispatchEvent(bitrateChangeEvent);
			// _profiler.end(uniqueIdentifier);
		}

		
		/*-........................Getters/setters............................*/
		
		public function get wholeVideoBytesTotal():Number{
			return _wholeVideoBytesTotal;
		}
		
		public function get duration():Number{
			return _duration;
		}
		
		/**
		 * Returns the time of video available in buffer. If the video 
		 * is streaming, return the end time of the stream.
		 */
		public function get bufferEndTime(){
			if(isStreaming){
				return _duration;
			} else {
				return _bytesToVideoTime(_serverSeekBytesStart +  _stream.bytesLoaded);
			}
		}
		
		/**
		 Returns percentage of how much video is loaded
 
		 @returns Number percentage
		 */
		public function get bufferedRatio(){
			if(_firstVideo.isStreaming){
				return 1;
			} else {
				var loadedRatio:Number =  _stream.bytesLoaded / _stream.bytesTotal;
				return loadedRatio;
			}
		}
		
		public function get currentBitrate():Number {
			return _currentBitrate;
		}
		
		public function get stream():NetStream {
			return _stream;
		}
		
		public function get progressiveStream():NetStream {
			return _netStream;
		}
		
		public function get streamingStream():DynamicStream {
			return _dynamicStream;
		}
		
		public function get videos():Array{
			return _videos;
		}
		
		/**
		 * Returns the first video of the video array. 
		 * Usually only the first video is needed, but if 
		 * the video is streaming and there are multiple different
		 * bitrates available then other videos are used as well
		 */
		public function get firstVideo():WebTVVideo{
			return _firstVideo
		}
		
		public function get isStreaming():Boolean{
			return _firstVideo.isStreaming;
		}
		
		public function get playheadTime():Number{
			return _stream.time;
		}
		
		public function get serverSeekAvailable():Boolean{
			return _firstVideo.serverSeekAvailable;
		}
		
		public function get bitratesAvailable():Array{
			if(_bitratesAvailable == null){
				_bitratesAvailable = new Array();
				for each (var video:WebTVVideo in _videos){
					var bitrate:Number = video.bitrate;
					if(!isNaN(bitrate) && bitrate > 0){
						_bitratesAvailable.push(bitrate);
					}
				}
			}
			return _bitratesAvailable;
		}
		
		public function get isAdvertisement():Boolean{
			return _firstVideo.isAdvertisement;
		}
		
		public function get isCompleted():Boolean {
			return _isCompleted;
		}
		
		public function get isPlaying():Boolean {
			return _isPlaying;
		}
	}
	
}
